OPC UA 驱动功能设计
OptiCloud IoT 平台 — OPC UA 协议驱动功能设计文档 · Eclipse Milo 0.6.16
第一次接触 OPC UA?别慌,这份文档从零开始,全程用生活化的比喻讲解。
一张图看懂 OPC UA 驱动
先看这张全景图建立整体认知,再看后续细节章节就不会"只见树木不见森林"。核心就一句话:驱动作为 Client 连到 Server,把数据采回来推给平台。
运行 OPC UA Server"] CONNECT["连接管理
Discovery → 安全通道 → Session"] COLLECT["数据采集
订阅 / 轮询 / 混合"] WRITE["指令下发
Write + READ_BACK 确认"] PC["PlatformClient
唯一通道"] QUEUE["点位队列"] BUSINESS["实时缓存 · 规则 · 告警
WebSocket · 历史库"] PLC -.->|"opc.tcp 连接"| CONNECT CONNECT --> COLLECT CONNECT --> WRITE COLLECT -->|"pushPointValue"| PC PC --> QUEUE --> BUSINESS BUSINESS -.->|"issuePointValue"| WRITE WRITE -.->|"Write 指令"| PLC style PLC fill:#e8f8e8,stroke:#34c759,color:#1c1c1e style CONNECT fill:#e8f0fe,stroke:#007aff,color:#1c1c1e style COLLECT fill:#f0e8fe,stroke:#5856d6,color:#1c1c1e style WRITE fill:#fff3e0,stroke:#ff9500,color:#1c1c1e style PC fill:#e0f7fa,stroke:#5ac8fa,color:#1c1c1e style QUEUE fill:#e0f7fa,stroke:#5ac8fa,color:#1c1c1e style BUSINESS fill:#fce8ec,stroke:#ff2d55,color:#1c1c1e
② 第 1-5 节协议入门建立直觉 →
③ 第 6-7 节看实例/点位属性 →
④ 跳到「5 分钟极简教程」动手
② 厂商适配指引查注意事项 →
③ 照「从零接入一台 Server」17 步操作 →
④ 出问题查「常见问题速查」
② 核心类导航找入口类 →
③ 结合数据采集链路理解流转 →
④ 安全档案章节看凭据实现
OPC UA 协议入门指南
从零开始理解工业自动化的「通用语言」 · IEC 62541
什么是 OPC UA
想象一个现代化工厂的车间,里面有各种设备:西门子 PLC、贝加莱控制器、各种品牌的传感器和仪表。每台设备都在产生数据——温度、压力、产量、能耗……但它们由不同厂家生产,各自"说着不同的方言",数据格式五花八门。
→ OPC UA 就是让所有工业设备"说同一种语言"的翻译官——工业 4.0 时代的"普通话"。
没有 OPC UA 之前,对接西门子 PLC 得用它的私有协议,换一台倍福的 PLC 又得换一套协议——集成商疲于奔命。OPC UA 出现后,大家"都说普通话",一套接口通吃主流工业设备。
OPC UA(OPC Unified Architecture,OPC 统一架构)是专为工业自动化设计的跨平台开放标准通信协议,由 OPC 基金会制定,国际标准编号 IEC 62541。它是经典 OPC(基于 Windows COM/DCOM)的全面升级,"UA" 就是"统一架构"的意思。
名字小故事:最初的 "OPC" 是 "OLE for Process Control"(基于微软 OLE 的过程控制),但 UA 版本已经完全脱离 Windows,跨平台、跨语言,名字只是个历史符号了。
| 角色 | 通俗理解 | 本驱动是谁 |
|---|---|---|
| OPC UA Server | 提供数据的"服务窗口"——PLC、SCADA、网关、第三方软件 | — |
| OPC UA Client | 主动去"取数据/发指令"的客户端 | 本驱动 |
| 驱动实例 | 页面里配置的一条 Server 连接 | 1 实例 = 1 Server 连接 |
| Variable | Server 里能采集/下发的数据点 | 导入后变成平台点位 |
重要:本驱动是 OPC UA Client,不提供 Server 能力。
地址空间——Server 的「图书馆」
想象 Server 内部有一座巨大的图书馆,里面所有的数据点都是"书"。这些书按树状结构分类摆放——有"楼层"(Objects 文件夹)、有"书架"(子文件夹)、有"书"(Variable 数据点)。
要找一本书,你需要它的索书号。在 OPC UA 里,这个"索书号"叫 NodeId。
Server 里所有东西都是 Node(节点)——数据点、文件夹、类型、视图……每个 Node 都有一个 NodeId 来定位自己。NodeId 由两部分组成:
| NamespaceIndex 命名空间下标 | 一个数字,类似"书在第几阅览室" 例: ns=2 |
| Identifier 标识内容 | 数字或字符串,类似"书在该阅览室的编号" 例: s=Machine/Temperature |
完整写法:ns=2;s=Machine/Temperature
关键陷阱:NamespaceIndex 不稳定!
Server 重启、升级、加载新信息模型后,原来的 ns=2 可能变成 ns=3。如果你把 ns=2;s=Temperature 死死存进数据库,下次连接可能就找错位置,甚至读到完全不相干的数据。
本驱动的解决:保存稳定的 NamespaceUri(如 urn:factory:line1),每次连接后查表反查当前编号,编号怎么变都不影响点位。
很多老系统直接存 ns=2;s=Temperature,Server 一重启编号变了,点位全失效甚至读到错误数据。本驱动从设计上规避了这个问题——你配置点位时填的是稳定的 NamespaceUri,不用关心数字编号。
地址空间里有多种 Node,但本驱动只关心 Variable——只有 Variable 才有"当前值"可供采集和下发。文件夹(Object)、类型(Type)、方法(Method)等都不能直接采值。
Object(不可采值)"] MACH --> TEMP["Temperature 温度
Variable ✓ 可采值"] MACH --> HUMI["Humidity 湿度
Variable ✓ 可采值"] MACH --> SETP["Setpoint 设定值
Variable ✓ 可采值"] style ROOT fill:#f2f2f7,stroke:#8e8e93,color:#1c1c1e style OBJ fill:#f0e8fe,stroke:#5856d6,color:#1c1c1e style MACH fill:#fff3e0,stroke:#ff9500,color:#1c1c1e style TEMP fill:#e8f8e8,stroke:#34c759,color:#1c1c1e style HUMI fill:#e8f8e8,stroke:#34c759,color:#1c1c1e style SETP fill:#e8f8e8,stroke:#34c759,color:#1c1c1e
你不知道 Server 里有什么数据点时,就需要 Browse——从根目录开始,一层层"翻文件夹",直到找到需要的 Variable。就像在图书馆查目录卡,定位到具体的书,再把它"借出来"(import)变成平台点位。
本驱动不会自动扫描整台 Server。正确流程是:Browse 逐层展开 → 人工选中需要的 Variable → 调 importNodes 导入。这保证你只采集关心的点,不会把 Server 里成千上万个无关节点全拖进来。
连接三步曲——从敲门到坐下
连 Server 前,先要知道它开放了哪些入口、要求什么安全策略、接受什么身份。这就是 Discovery——相当于看 Server 门口的"营业告示牌"。返回的每一条信息叫一个 Endpoint,告诉你:
| Endpoint 包含的信息 | 通俗理解 |
|---|---|
| Endpoint URL | "本入口的地址",如 opc.tcp://10.0.0.8:4840 |
| SecurityPolicy | "用什么加密算法",如 NONE / Basic256Sha256 |
| MessageSecurityMode | "要不要签名/加密",如 NONE / SIGN / SIGN_AND_ENCRYPT |
| 支持的 IdentityType | "允许谁进",如 ANONYMOUS / USERNAME / X509 |
| Server 证书指纹 | "Server 的身份证号"(SHA-256) |
选好 Endpoint 后,第二步建立 SecureChannel(安全通道)。这一步会:
· 校验 Server 证书是否可信
· 协商加密算法
· 如果用证书,Client 也要出示自己的证书
相当于"过安检"——确认双方身份、约定好怎么加密通信。
安检过后,第三步建立 Session(会话)。Session 是 Client 登录后获得的"长期通行证",期间的所有读写订阅都在这个会话上进行。
相当于"登记入座"——之后所有的服务请求都凭这张通行证办理。Session 有超时时间,需要定期"续证"(KeepAlive)。
| 身份类型 | 通俗理解 | 需要提供 | 能否用无安全 Endpoint |
|---|---|---|---|
| ANONYMOUS 匿名 | "访客"——谁都能进 | 无需任何凭据 | 可以 |
| USERNAME 用户名密码 | "持票观众"——出示门票 | username + 密码引用 | 禁止 |
| X509 证书 | "VIP 持卡人"——刷卡进入 | 客户端证书 + 安全档案 | 禁止 |
安全规则:USERNAME 不允许通过无加密的 Endpoint 发送(密码会明文泄露)。
Discovery = 看大厅公告牌(哪些窗口开放、需要什么材料)→ SecureChannel = 过安检门(出示身份证、包过扫描仪)→ Session = 到窗口取号排队办事(之后所有业务都在这个号下办理)。三步缺一不可,顺序也不能乱。
SecurityPolicy(加密算法)和 MessageSecurityMode(消息模式)必须配对使用,组合规则严格:
不加密不签名"] SIGN_P["非 NONE 安全策略"] --> SIGN_M["Mode = SIGN
仅签名"] SIGN_P --> SE_M["Mode = SIGN_AND_ENCRYPT
签名+加密
推荐"] style NONE_P fill:#f2f2f7,stroke:#8e8e93,color:#1c1c1e style NONE_M fill:#f2f2f7,color:#1c1c1e style SIGN_P fill:#e8f8e8,stroke:#34c759,color:#1c1c1e style SIGN_M fill:#e0f7fa,stroke:#5ac8fa,color:#1c1c1e style SE_M fill:#e8f0fe,stroke:#007aff,color:#1c1c1e
支持的策略:NONE、Basic128Rsa15、Basic256、Basic256Sha256、Aes128_Sha256_RsaOaep、Aes256_Sha256_RsaPss。实际可用以 getEndpoints 返回为准。
订阅——Server 的「公众号推送」
Client 每隔 N 秒主动问 Server:"温度是多少?"
Server 答:"25.6"。
N 秒后再问:"温度是多少?"
Server 答:"25.6"。
问题:大部分时候没变化,白白浪费带宽;且最多有 N 秒延迟。
Client 跟 Server 说:"帮我盯着温度,变化超过 0.5 就告诉我。"
Server 答:"好的。"
温度从 25.6 变为 26.2(变化 0.6 > 0.5):
Server 主动推送:"温度变了,现在 26.2!"
优势:不浪费带宽,几乎零延迟——就像关注了"公众号"自动收推送。
订阅机制分两层:Subscription(订阅)是"推送频道",MonitoredItem(监控项)是频道里"具体被盯着的点"。一个 Subscription 可以挂多个 MonitoredItem。
两个关键参数:发布周期(Server 多久推一次通知)、采样周期(Server 多久测一次值)。用户配的是 requested 值,Server 可能修订为它能支持的实际值。
| 对比维度 | SUBSCRIPTION 订阅 | POLLING 轮询 | HYBRID 混合 |
|---|---|---|---|
| 原理 | Server 主动推变化 | Client 定时全量读 | 订阅 + 定时核对 |
| 实时性 | 高——变化即推 | 取决于轮询周期 | 高 |
| 网络开销 | 低——仅推变化 | 高——每次全读 | 较高 |
| 可靠性 | 依赖 Server 订阅实现 | 简单可靠 | 最强(订阅漏报有核对兜底) |
| 适用场景 | 大多数正式 Server | 订阅不稳的 Server | 强一致性要求场景 |
| 默认 | 本驱动默认 |
策略:SUBSCRIPTION 模式下,单个 MonitoredItem 创建失败的点会自动降级为轮询,不影响其他点。
大多数情况用默认的 SUBSCRIPTION。如果发现订阅不更新(某些 Server 实现不完整),切 POLLING 验证是不是 Server 的问题;如果对数据一致性要求极高(不能漏报),用 HYBRID 双保险。
每个采样值都附带一个 StatusCode,表示这个数据"靠不靠谱"。OPC UA 分三档,本驱动映射到平台的 statusFlag:
| OPC UA 质量 | 含义 | 平台 statusFlag |
|---|---|---|
| Good | 数据可靠,可直接使用 | 1 |
| Uncertain | 数据存疑(如传感器超出量程) | 7 |
| Bad | 数据不可用(如通信失败) | 2 |
Bad 且没有可用值的样本,本驱动不会向平台推 null,只保留原始诊断信息。
OPC UA 协议特点总结
驱动整体架构
OptiCloud 启动时,通过 Java SPI 机制自动发现并加载驱动。OPC UA 驱动的注册入口在 SPI 文件中固定一行:
驱动采用清晰的分层设计,每层职责单一。每个实例独立连接、独立 Session、独立 Subscription,互不影响——即使 Endpoint 相同也不跨项目共享。
SPI 入口,只做参数补全和委托"] MGR["OpcUaInstanceManager
管理全部实例、心跳对账、API 路由"] H1["Holder #101
单实例状态机"] H2["Holder #202
单实例状态机"] H3["Holder #303
单实例状态机"] ACCESS --> MGR MGR --> H1 & H2 & H3 H1 --> C1["Milo Client / Session / Subscription"] H2 --> C2["Milo Client / Session / Subscription"] H3 --> C3["Milo Client / Session / Subscription"] style ACCESS fill:#e8f0fe,stroke:#007aff,color:#1c1c1e style MGR fill:#f0e8fe,stroke:#5856d6,color:#1c1c1e style H1 fill:#e8f8e8,stroke:#34c759,color:#1c1c1e style H2 fill:#e8f8e8,stroke:#34c759,color:#1c1c1e style H3 fill:#e8f8e8,stroke:#34c759,color:#1c1c1e
| 层 | 职责 |
|---|---|
| Access | 平台 SPI 入口,只做参数补全和委托 |
| Manager | 管理全部实例、实例级锁、退避、批量写入分组、API 路由 |
| Holder | 一个实例的完整运行状态:连接、点位快照、订阅、轮询、推送、重连 |
| Core | 协议操作和类型转换,不感知 Web Controller |
| PlatformClient | 驱动访问平台数据的唯一通道 |
或 Polling Read"] B --> C["OpcUaSampleConverter
类型/质量转换"] C --> D["LatestValueBuffer
按 pointId 合并最新"] D --> E["500ms 虚拟刷新线程"] E --> F["PlatformClient.pushPointValue"] F --> G["PointQueueManager"] G --> H["缓存/规则/告警/
WebSocket/历史链路"] style C fill:#e0f7fa,stroke:#5ac8fa,color:#1c1c1e style D fill:#fff3e0,stroke:#ff9500,color:#1c1c1e
关键设计:Milo 回调线程只做转换和缓冲,不直接调用平台,防止平台慢操作阻塞 OPC UA Publish 线程。
想象快递员(Milo 回调)和分拣中心(平台)。如果快递员每收一个包裹就亲自跑去送客户(直接调用平台),路上耽误的时间会让后面的包裹堆积。加个"暂存架"(LatestValueBuffer),快递员放下就走,专门的配送员(500ms 刷新线程)批量送——效率高,谁也不堵谁。
| 路径 / 类 | 作用 |
|---|---|
access/OpcUaAccess | DriverAccess 实现和 SPI 入口 |
access/OpcUaInstanceManager | 多实例生命周期、心跳对账、API 和写入分发 |
access/OpcUaInstanceHolder | 单实例状态机、采集线程、重连和热更新 |
core/OpcUaConfigParser | 所有字符串属性的强类型解析和统一校验 |
core/OpcUaClientFactory | Discovery、Endpoint 选择、安全身份、连接 |
core/OpcUaNodeResolver | NamespaceUri 稳定地址与 NodeId 转换 |
core/OpcUaBrowser | 分页 Browse 和 ContinuationPoint 释放 |
core/OpcUaNodeImporter | Server 元数据复核和平台散点创建 |
core/OpcUaPointReader | 批量 Read 和 TooManyOperations 自动折半 |
core/OpcUaSubscriptionController | Subscription、MonitoredItem 和单点轮询降级 |
core/OpcUaPointWriter | 批量 Write、逐点 StatusCode 和 READ_BACK |
storage/OpcUaLatestValueBuffer | 每点只保存最新待推送样本 |
task/OpcUaAliveTask | 每 4 秒实例/点位对账、心跳和重连 |
实例属性(22 项)
| 属性 code | 默认值 | 说明 |
|---|---|---|
endpointUrl | 必填 | 实际 Endpoint URL,如 opc.tcp://10.0.0.8:4840 |
serverVendor | AUTO | AUTO / GENERIC / SIEMENS / BECKHOFF / KEPSERVEREX / CODESYS / IGNITION / SCHNEIDER / CUSTOM |
securityPolicy | NONE | Server Endpoint 的安全策略 |
messageSecurityMode | NONE | NONE / SIGN / SIGN_AND_ENCRYPT |
identityType | ANONYMOUS | ANONYMOUS / USERNAME / X509 |
username | 空 | USERNAME 时必填 |
passwordSecretRef | 空 | 正式 USERNAME 必填,只保存引用不存明文 |
securityProfileId | 空 | 安全 Endpoint 或 X509 时必填 |
安全组合铁律:NONE 策略只能配 NONE 模式;非 NONE 必须启用 SIGN 或 SIGN_AND_ENCRYPT;USERNAME 禁止用无安全 Endpoint。
安全组合的正确来源只有一个:先调 getEndpoints 看 Server 实际公告什么,再把选中的完整组合存进实例。千万不要手猜 SecurityPolicy URI——猜错了连接必然失败,而且报错信息不一定直观。
| 属性 code | 默认值 | 范围 / 说明 |
|---|---|---|
connectionTimeoutMs | 10000 | 1000 ~ 120000 |
requestTimeoutMs | 10000 | 1000 ~ 120000 |
sessionTimeoutMs | 60000 | 10000 ~ 3600000 |
collectMode | SUBSCRIPTION | SUBSCRIPTION / POLLING / HYBRID |
publishingIntervalMs | 1000 | 50 ~ 60000,Server 可修订 |
defaultSamplingIntervalMs | 1000 | 0 ~ 60000,0 表示尽可能快 |
defaultQueueSize | 1 | 1 ~ 1000 |
discardOldest | true | 队列满时是否丢弃最旧值 |
pollIntervalMs | 5000 | 100 ~ 3600000 |
batchSize | 100 | 1 ~ 300 |
initialReadEnabled | true | 建立采集器前读取当前值 |
writeConfirmMode | READ_BACK | NONE / READ_BACK |
reconnectEnabled | true | 是否允许自动重连 |
maxReconnectDelayMs | 60000 | 4000 ~ 300000 |
点位属性(12 项)
| 属性 code | 说明 |
|---|---|
namespaceUri | 稳定命名空间 URI,必填(替代易变的 ns 数字) |
identifierType | NUMERIC / STRING / GUID / OPAQUE |
identifier | NodeId 标识内容;OPAQUE 使用 Base64 |
attributeId | 固定 VALUE(当前只采值属性) |
indexRange | 必须为空(不支持数组切片) |
samplingIntervalMs | 可选,覆盖实例默认,0 ~ 60000 |
queueSize | 可选,覆盖实例默认,1 ~ 1000 |
discardOldest | 可选,覆盖实例默认 |
deadbandType | NONE / ABSOLUTE / PERCENT |
deadbandValue | ABSOLUTE 非负;PERCENT 为 0 ~ 100 |
serverDataTypeNodeId | 导入时记录的诊断信息,运行时以 Server 为准 |
valueRank | 固定 -1(表示标量,不支持数组) |
PERCENT 死区依赖 Variable 的 EURange,缺 EURange 时该点会标记失败并转轮询。
本驱动持久化点位时,不保存易变的 ns=2,而是保存四元组:
每次连接成功后读取 NamespaceArray,把 NamespaceUri 解析回当前的 NodeId。这样即使 Server 重启导致编号从 ns=2 变成 ns=3,点位也无需修改,自动重新定位。
| OPC UA 类型 | 平台类型 |
|---|---|
| Boolean | bool |
| SByte / Byte / Int16 / UInt16 / Int32 | int |
| Float / Double | double |
| Enumeration | enum |
| UInt32 / Int64 / UInt64 | text(避免精度损失) |
| String / XmlElement / LocalizedText / Guid / DateTime | text |
DateTime 转为 UTC ISO-8601;数组、ByteString、Structure 不做隐式 toString。
点位唯一码格式(名称不参与判断,改名不重建):
幂等保证:同一节点重复导入返回 SKIPPED,不会创建第二个点位。
四大驱动 API
统一路由前缀 POST /driver/api/opcua/{api},请求使用表单参数(nodes 是 JSON 数组字符串)。Controller 自动从项目上下文写入 projectId。
| API | 作用 | 前置条件 |
|---|---|---|
| getEndpoints | Discovery,列出 Endpoint 组合 | 不需要实例 |
| testConnection | 临时试连,结束立即关闭 | 不需要正式实例 |
| browseNodes | 用正式 Session 分页浏览节点树 | 实例 RUNNING |
| importNodes | 复核元数据并保存为散点 | 实例 RUNNING |
输入 Discovery URL,返回 Server 公告的所有 Endpoint(URL、安全策略、消息模式、身份类型、证书指纹)。
正确顺序:先 getEndpoints → 用户选择完整组合 → 保存到实例。
不能默认用返回列表第一项。
用与正式连接相同的配置建立一次临时连接,读取 NamespaceArray 和 ServerInfo。
关键:返回前一定在 finally 中关闭临时 Client。成功只证明参数能建立连接,不能用于后续浏览/采集/下发。
| 参数 | 默认值 | 说明 |
|---|---|---|
instanceId | 必填 | 必须属于当前项目且 RUNNING |
expandedNodeId | RootFolder | 要展开的节点(稳定 ExpandedNodeId) |
direction | FORWARD | FORWARD / INVERSE / BOTH |
nodeClasses | 全部 | 逗号分隔,如 OBJECT,VARIABLE |
pageSize | 100 | 1 ~ 200 |
continuationToken | 空 | 下一页随机 token,只能消费一次 |
releaseContinuationPoint | false | true 表示取消分页并释放 Server 游标 |
返回节点的 selectable=true 表示当前用户可读/可写的标量 Variable——只有这种才能导入。
提交 Browse 选中的 ExpandedNodeId,驱动重新从 Server 读取真实元数据(NodeClass、DataType、权限、单位、量程),只导入可读/可写的标量 Variable。
导入只创建散点,不创建设备类型/设备/自动绑定,不修改数据库表结构。
| 操作 | 是否建 Session | Session 是否保留 | 能否证明正式实例已连接 |
|---|---|---|---|
| getEndpoints | 否 | 否 | 否 |
| testConnection | 是 | 否(finally 关闭) | 否(只证明参数可用) |
| openDriverInstance | 是 | 是(长期持有) | 需配合状态+Browse 验证 |
正式连接成功的三项铁证:① 日志"实例已连接" ② 一个心跳后 instanceStatus=1 ③ browseNodes 能浏览到节点。
实例生命周期与状态机
| 状态值 | 状态 | 含义 |
|---|---|---|
0 | CLOSED | 实例开关关闭,没有正式 Session |
1 | RUNNING | Holder、Client 和正式 Session 正常 |
3 | OFFLINE | 已启用但连接不可用,或仍在退避重试 |
增加运行代次"] --> S2["2 停止轮询和刷新虚拟线程
并等待退出"] S2 --> S3["3 删除 Subscription"] S3 --> S4["4 清理最新值缓冲和点位快照"] S4 --> S5["5 释放全部 Browse ContinuationPoint"] S5 --> S6["6 关闭 Session 和 Milo Client"] S6 --> S7["7 状态切换为 CLOSED"] style S1 fill:#fff3e0,stroke:#ff9500,color:#1c1c1e style S7 fill:#f2f2f7,stroke:#8e8e93,color:#1c1c1e
运行代次(generation)机制:阻止旧回调在新连接建立后继续推值,避免"幽灵数据"。
AliveTask 每 4 秒检查实例、Session、Subscription Publish、点位指纹和凭据 revision。失败按 4/8/16/32/60 秒退避,受 maxReconnectDelayMs 限制,避免高频冲击 Server。
等待4秒"] --> F2["失败2次
等待8秒"] F2 --> F3["失败3次
等待16秒"] F3 --> F4["失败4次
等待32秒"] F4 --> F5["失败5+次
等待60秒封顶"] F5 --> RCV{"重连成功?"} RCV -->|"Yes"| RST["重置退避
恢复采集"] RCV -->|"No"| F5 style F1 fill:#fff3e0,stroke:#ff9500,color:#1c1c1e style F5 fill:#ffe5e5,stroke:#ff3b30,color:#1c1c1e style RST fill:#e8f8e8,stroke:#34c759,color:#1c1c1e
如果 Server 挂了,每 4 秒重连一次等于持续"砸门"——既浪费资源,也可能拖垮正在恢复的 Server。退避策略像"敲门没人应,下次多等一会儿再敲":第一次等 4 秒,再失败等 8 秒……最长 60 秒一次。Server 恢复后立即重置,马上恢复采集。
点位下发流程
+ Server UserAccessLevel"] P5 --> P6["6 按实时 Server DataType 严格转换"] P6 --> P7["7 批量 Write 并逐项检查 StatusCode"] P7 --> P8["8 READ_BACK 模式只读 Write 成功项"] P8 --> P9["9 规范化目标值 = 读回值 才算成功"] style P1 fill:#e8f0fe,stroke:#007aff,color:#1c1c1e style P9 fill:#e8f8e8,stroke:#34c759,color:#1c1c1e
只读、错误类型、越界或 Bad StatusCode 都不会被记为成功。
有些设备收到 Write 指令返回"成功",但内部业务逻辑会改写或钳位你的值(比如设 100 但设备上限是 80)。如果只看 Write 结果,平台会显示一个从未真实生效的值。READ_BACK 模式重新读一次真实值,平台显示的永远是设备里真实的数——宁可显示失败,不报虚假的成功。
Write 成功后重新读 Server 实际值,规范化比较目标值与读回值,相等才推送确认值到平台。
优势:平台显示的值是 Server 真实生效的值,不是乐观假设。
只检查 Write 的 StatusCode,不回读。
注意:不会把目标值乐观推入平台,需等待后续订阅或轮询更新。适合对实时确认不敏感的场景。
安全档案与凭据管理
USERNAME 连接需要密码,但本驱动绝不存明文密码。密码加密保存在密钥环里,实例属性只保存一个 secretRef 引用——相当于只记下"钥匙牌的编号",不存钥匙本身。
opti.opcua.security"] B -->|"生成引用"| C["secretRef
如 sec_xxx"] C -->|"实例属性只保存这个"| D["passwordSecretRef"] style A fill:#ffe5e5,stroke:#ff3b30,color:#1c1c1e style B fill:#f0e8fe,stroke:#5856d6,color:#1c1c1e style C fill:#e8f8e8,stroke:#34c759,color:#1c1c1e style D fill:#e8f0fe,stroke:#007aff,color:#1c1c1e
接口:POST /iot/opcua/security/secrets(创建)· PUT .../secrets/{secretRef}(轮换)· DELETE(删除,被引用时拒绝)。
安全 Endpoint 需要客户端证书建立 SecureChannel。安全档案是集中保管证书的容器,使用 multipart/form-data 上传:
| 字段 | 必填 | 含义 |
|---|---|---|
profileName | 必填 | 档案显示名 |
applicationUri | 可选 | 应与客户端证书 SAN URI 一致 |
pkcs12File | 必填 | 含客户端私钥+证书链的 PKCS12/PFX |
pkcs12Password | 必填 | PKCS12 文件密码 |
trustedCertificates | 可选 | 目标 Server 证书或签发 CA(可多文件) |
crlFiles | 可选 | 证书吊销列表 |
接口会校验 PKCS12 可读性、私钥匹配、证书有效期、Application URI、信任证书等。返回不含私钥/PKCS12/密码正文。
实例属性里保存的 passwordSecretRef 和 securityProfileId 只是"引用编号",真实密码和私钥加密存在密钥环里。这样即使数据库泄露,攻击者拿到的也只是一堆无意义的编号,无法还原出原始凭据。
Secret 或 Profile 支持在实例运行时直接更新,不需要手动停实例。但更新后驱动会对引用该材料的实例做一次冷重连——期间可能短暂断采:
建议先 POST .../profiles/{id}/validate 校验材料再轮换;轮换失败实例保持 OFFLINE,需修正材料后重试。
生产环境安全连接涉及三类文件,不是同一个东西,也不能相互推导:
| 文件 | 内容 | 用途 |
|---|---|---|
| server.der | Server 公钥证书 | 上传到 trustedCertificates,校验 Server 身份 |
| client.p12 | Client 私钥+证书链 | 上传到 pkcs12File,建立 SecureChannel |
| client.der | Client 公钥证书 | 导入 Server 信任列表(由 Server 管理员操作) |
必须建立双向信任:Client 信任 server.der;Server 信任 client.der(或其签发 CA)。
部署与测试
| JDK | 21+ |
| 协议库 | Eclipse Milo Client 0.6.16 |
| 驱动版本 | 1.0.0 |
| 驱动名 | 固定 opcua |
| 运行方式 | 普通 JAR + target/lib |
| 接入模式 | EMBEDDED(内嵌,共享 JVM) |
应用启动时自动起一个本机 Server 用于联调:
NamespaceUri: urn:sctech:opti:opcua:long-running-test
账号 / 密码: 见内部交付物(测试凭据不写入公开文档)
只绑定 127.0.0.1,仅用于本机联调;不代表真实厂商支持 Basic256Sha256。
| 节点 | 类型 | accessMode | Server 行为 |
|---|---|---|---|
| Temperature | double | 只读 | 20.0~29.9 循环变化,验证订阅 |
| Humidity | double | 只读 | 固定值 |
| Setpoint | double | 读写 | 默认值稳定,适合下发测试 |
| Running | bool | 读写 | 开关状态 |
| Counter | int | 读写 | 每秒递增 |
| Message | text | 读写 | 字符串 |
常见问题速查
| 现象 | 检查方向 |
|---|---|
| getEndpoints 失败 | URL 是否绝对 URI;端口/防火墙/DNS;Server 是否启动 |
| "未找到完全匹配的 Endpoint" | 保存的 URL/策略/模式/身份与 Discovery 返回不一致;不要手猜 SecurityPolicy |
| USERNAME/X509 被拒绝 | CredentialProvider 是否安装;secretRef/profileId 是否属当前项目;证书有效期/信任链 |
| 实例 RUNNING 但没数据 | initialRead 是否返回值;订阅点是否降级轮询;临时切 HYBRID 排查 Publish 兼容性 |
| 现象 | 检查方向 |
|---|---|
| 只支持 Variable | Object/Method/Type 不能导入 |
| valueRank 必须 -1 | 数组/矩阵不支持 |
| 权限不足 | 当前用户需 CurrentRead 或 CurrentWrite |
| PERCENT 死区失败 | 要求 Server 提供 EURange |
| 容量不足 | 有效节点整批不保存 |
| 现象 | 检查方向 |
|---|---|
| 浏览 token 无效 | token 只能用一次;120 秒过期;实例重连后旧 token 失效 |
| 订阅不更新 | 测试 Server 的 SubscriptionModel 未委托;切 POLLING 验证 Server 节点正常 |
| 实时值有但无历史 | 驱动不写 InfluxDB;检查平台历史开关/PointQueueManager/InfluxDB 配置 |
| 现象 | 检查方向 |
|---|---|
| "点位的驱动实例未运行" | 查 paginatePoint 记录 driverInstanceId;查 getDriverWithInstanceList 确认该实例 instanceStatus=1(不是同名/同项目其他实例) |
| 写入失败 | accessMode 是否含 w;UserAccessLevel 是否含 CurrentWrite;类型能否严格转换;READ_BACK 是否读到不同值 |
| HTTP success=true 但值没变 | 接口 success 只表示"已调用驱动",必须看 curValue/curValueTime/日志 |
排查"实际运行但提示未运行"的关键:以 paginatePoint 的 driverInstanceId + getDriverWithInstanceList 的 instanceStatus=1 为准,不要凭页面图标或同名实例判断。
从零接入一台 Server
前面的章节讲了 OPC UA 的概念和单个 API,但新员工最常问的是:"我到底该按什么顺序操作?" 这一节把从零到采到数据的完整流程串成一条线,照着做就能跑通。
下面以平台内置的 Milo 测试 Server 为例(opc.tcp://localhost:4840/opti-test),完整演示从查驱动 ID 到验证采集的每一步。所有 HTTP 请求都要带登录认证 + Project-Id 请求头。
启动日志应包含 opc.tcp://localhost:4840/opti-test。Windows 可用 netstat -ano | findstr :4840 检查端口监听。
测试 Server 只绑定本机回环地址,驱动和 Server 必须在同一台主机。容器或其他机器不能用 localhost。
GET /iot/driver/getDriverWithInstanceList,在返回里找 driverName=opcua,记录它的 driverId(示例 9)。
不能把驱动名 "opcua" 当作 driverId 使用。
POST /driver/api/opcua/getEndpoints
参数:discoveryUrl=opc.tcp://localhost:4840/opti-test
返回 Endpoint 列表,记录完整的 URL + 安全策略 + 模式 + 身份类型组合。
POST /driver/api/opcua/testConnection
用第 ③ 步选中的完整配置,建立临时连接验证参数可用。
成功只证明参数能建立连接,不能用于后续浏览/采集/下发。
POST /iot/driver/saveInstance,instanceSwitchOn=false。
把"保存配置"和"建立连接"分开,便于定位错误。
不用 username/password 的匿名连接,这些字段建议显式传空字符串而非 JSON null,避免平台属性转 Map 时空指针。
再次调 getDriverWithInstanceList,在 instanceList 里按名称找到新实例的 instanceId(如 2079755617307570178)。
POST /iot/driver/openDriverInstance/{instanceId}
平台发布打开事件,驱动创建长期 Session。
接口返回成功只表示流程没抛异常,底层失败不会传到 Controller。必须继续第 ⑧ 步确认状态。
等 1 个心跳周期(约 4 秒),查 getDriverWithInstanceList,目标实例应返回 instanceStatus=1(RUNNING)。
短暂 OFFLINE 正常,持续 OFFLINE 查驱动日志。
POST /driver/api/opcua/browseNodes,从 RootFolder 逐层展开到 Scalar 目录,找到 6 个测试 Variable。
记录要导入点的 expandedNodeId,例如 nsu=urn:sctech:opti:opcua:long-running-test;s=Scalar/Temperature。
POST /driver/api/opcua/importNodes,把选中的 expandedNodeId 作为 nodes JSON 数组提交。
驱动重新读 Server 元数据校验,只导入可读/可写标量 Variable。
POST /iot/driver/paginatePoint 查询新点位。initialReadEnabled=true 时应立即看到当前值。
① 日志"实例已连接" ② instanceStatus=1 ③ curValue 有值且 curValueTime 持续更新。三项齐全才算真正跑通。
5 分钟极简教程
用平台内置的 Milo 测试 Server,跳过复杂的证书配置,5 分钟内体验一次完整的"接入 → 采集 → 看到数据"。
平台已启动(日志能看到 Milo 测试 Server 启动);你有一个启用状态的测试项目;本节跳过安全配置,用匿名连接。
在平台创建 OPC UA 驱动实例时,只需要关心这几个关键值,其余用默认:
| 属性 | 填什么 | 为什么 |
|---|---|---|
endpointUrl | opc.tcp://localhost:4840/opti-test | 测试 Server 地址 |
securityPolicy | NONE | 测试用,不加密 |
messageSecurityMode | NONE | 配合 NONE 策略 |
identityType | ANONYMOUS | 匿名身份最简单 |
username | 空字符串 "" | 匿名不需要 |
passwordSecretRef | 空字符串 "" | 匿名不需要 |
securityProfileId | 空字符串 "" | 无安全档案 |
✓ 其余 15 个属性全用默认值即可。
switchOn=false"] --> B["② 启用实例
switchOn=true"] B --> C["③ 浏览到 Scalar
导入 Temperature"] C --> D["④ 查询点位
看到温度变化"] style A fill:#e8f0fe,stroke:#007aff,color:#1c1c1e style D fill:#e8f8e8,stroke:#34c759,color:#1c1c1e
instanceSwitchOn=false 保存。记住实例名。Objects → Scalar,选 Temperature 点导入。curValue 应在 20.0~29.9 间变化。看到温度值变化,说明 Discovery → Session → 订阅 → 推送整条链路都通了。接下来可以尝试导入 Setpoint 并下发一个值,体验完整的读写闭环。
| 卡在哪一步 | 检查什么 |
|---|---|
| 实例一直 OFFLINE | 看日志有无"实例已连接";端口 4840 是否监听 |
| 浏览不到 Scalar | 从 RootFolder → Objects → 逐层展开,方向选 FORWARD |
| 导入返回 FAILED | 确认选的是 Variable(selectable=true),不是 Object 文件夹 |
| 导入成功但值不变 | 切 collectMode=POLLING 验证(测试 Server 订阅可能有已知问题) |
协议选型对比——什么时候用 OPC UA
新员工常问:"同样是接设备,OPC UA、Modbus、MQTT 我该选哪个?" 看场景定:
| 维度 | OPC UA | Modbus | MQTT |
|---|---|---|---|
| 适合场景 | 复杂信息模型、安全要求高、PLC/SCADA | 简单寄存器读写、仪表/变频器 | 物联网网关消息上报 |
| 数据组织 | 树状地址空间(灵活) | 线性寄存器表(简单) | Topic 订阅(松耦合) |
| 采集方式 | 订阅为主(实时) | 主从轮询 | 发布订阅 |
| 安全性 | 内置证书 + 加密(强) | 弱(无内置安全) | 依赖 TLS(中等) |
| 厂商支持 | 主流 PLC 原生支持 | 几乎所有设备支持 | 网关/IoT 设备 |
| 配置复杂度 | 中(需 Browse + 导入) | 低(填地址即可) | 低(配置 Topic) |
| 本平台驱动 | 本文档 | Modbus TCP/RTU | MQTT 网关 |
或安全要求高?"} A1 -->|"是"| A2["✓ OPC UA 是最佳选择"] A1 -->|"简单读写"| A3["OPC UA 也能胜任
但 Modbus 更轻量"] style A2 fill:#e8f8e8,stroke:#34c759,color:#1c1c1e
设备原生支持 OPC UA 就优先用 OPC UA(信息模型清晰、安全强);老旧设备只有 Modbus 接口就用 Modbus;网关汇聚多设备用 MQTT。三者不是互斥的,一个项目可以混用多个驱动。
常见厂商适配指引
serverVendor 属性支持指定厂商档案,帮助驱动做兼容性处理。以下是几个常见厂商的接入经验:
Endpoint:默认
opc.tcp://IP:4840注意:需在 TIA Portal 启用 OPC UA Server;证书模式建议 Basic256Sha256;证书要导入 PLC 信任列表
Endpoint:
opc.tcp://IP:4840注意:TwinCAT 自带 OPC UA Server;变量需勾选 "OPC UA" 发布;符号名即 NodeId 标识
Endpoint:
opc.tcp://IP:49320(非默认端口)注意:常见端口是 49320 不是 4840;空间按"通道.设备.标签"分层
Endpoint:
opc.tcp://IP:4096注意:标签路径即 NodeId;支持匿名但生产建议开安全
Endpoint:
opc.tcp://IP:4840注意:需在工程添加 OPC UA Server 设备;变量要勾选发布到 OPC UA
Endpoint:
opc.tcp://IP:4840注意:需在 Control Config 里启用 OPC UA;变量需显式发布
① OPC UA Server 已启用(很多设备默认关闭)→ ② 知道 Endpoint 地址和端口(不一定是 4840)→ ③ 决定用匿名还是凭据(生产建议凭据)→ ④ 安全连接需准备双向证书 → ⑤ 确认要采的变量已发布到地址空间。
术语速查表
OPC UA 术语密度高,遇到不记得的概念,回这张表查就行。
| 术语 | 生活比喻 | 说明 |
|---|---|---|
Discovery URL | 门口告示牌 | 查询 Server 提供哪些连接入口的地址 |
Endpoint | 入口详情 | 实际连接地址 + 安全策略 + 消息模式 + 身份方式 |
SecureChannel | 过安检 | 对消息签名/加密/校验证书的安全通道 |
Session | 取号办事 | Client 登录后的逻辑会话,期间操作都在此会话上 |
Address Space | 图书馆 | Server 暴露的一棵节点树 |
Node / NodeId | 书 / 索书号 | 节点 / 当前会话内定位节点的地址 |
NamespaceIndex | 阅览室编号 | NodeId 的数字下标,不稳定,重启可能变 |
NamespaceUri | 阅览室门牌 | 稳定的命名空间 URI,本驱动用它替代易变的编号 |
ExpandedNodeId | 带全址的索书号 | 携带 NamespaceUri 的稳定节点表示 |
Variable | 能读值的书 | 有当前值可供采集/下发的节点 |
Subscription | 推送频道 | Server 按发布周期推送变化数据的订阅 |
MonitoredItem | 被盯的点 | Subscription 中具体被监控的 Variable |
DataValue | 完整包裹 | 值 + StatusCode + 时间戳的组合 |
StatusCode | 质量标签 | 数据质量 Good / Uncertain / Bad |
ContinuationPoint | 翻页游标 | 分页 Browse 的临时游标,用完必须释放 |
已知边界
新增厂商兼容应实现 vendor profile 并记录厂商/产品/版本/测试日期;不能用散落 if/else、关证书验证或动态 SQL 解决现场差异。